package cn.xenosp.server.module.activiti.service.impl;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.xenosp.server.common.Constant;
import cn.xenosp.server.common.error.ActivitiBusinessException;
import cn.xenosp.server.common.tools.IdUtil;
import cn.xenosp.server.common.tools.ObjectConvert;
import cn.xenosp.server.common.tools.XmlUtil;
import cn.xenosp.server.config.activiti.properties.ActivitiProperties;
import cn.xenosp.server.module.activiti.entity.ActivitiModel;
import cn.xenosp.server.module.activiti.entity.ProcessInstanceVO;
import cn.xenosp.server.module.activiti.entity.ProcessDefinitionVO;
import cn.xenosp.server.module.activiti.entity.SuspendDefinition;
import cn.xenosp.server.module.activiti.mapper.ActReProcdefMapper;
import cn.xenosp.server.module.activiti.service.ModelService;
import cn.xenosp.server.module.activiti.service.ProcessDefinitionService;
import cn.xenosp.server.module.user.entity.SysUser;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.activiti.bpmn.converter.BpmnXMLConverter;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.core.common.project.model.ProjectManifest;
import org.activiti.editor.language.json.converter.BpmnJsonConverter;
import org.activiti.engine.HistoryService;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.RuntimeService;
import org.activiti.engine.TaskService;
import org.activiti.engine.impl.persistence.entity.ProcessDefinitionEntityImpl;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentBuilder;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.repository.ProcessDefinitionQuery;
import org.activiti.engine.runtime.ProcessInstance;
import org.activiti.engine.runtime.ProcessInstanceQuery;
import org.activiti.engine.task.Task;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.exceptions.PersistenceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.SQLIntegrityConstraintViolationException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.zip.ZipInputStream;

/**
 * 项目名： service-web
 * 包路径： cn.xenosp.server.module.activiti.service.impl
 * 作者：   TongHui
 * 创建时间: 2025-03-12 16:43
 * 描述: TODO
 * 版本: 1.0
 */
@Slf4j
@Service
public class ProcessDefinitionServiceImpl extends ServiceImpl<ActReProcdefMapper,ProcessDefinitionVO> implements ProcessDefinitionService {

    // 管理和控制流程定义的服务接口，包括部署、查询和删除流程定义等功能；
    @Autowired
    private RepositoryService repositoryService;

    @Autowired
    private ModelService modelService;

    @Autowired
    private ActivitiProperties activitiProperties;
    //运行时的接口
    @Autowired
    private RuntimeService runtimeService;
    // 任务处理接口
    @Autowired
    private TaskService taskService;
    // 历史处理接口
    @Autowired
    private HistoryService historyService;


    @Override
    public IPage getByPaged(ProcessDefinitionVO processDefinitionVO) {
        Page page = new Page(processDefinitionVO.getPage(), processDefinitionVO.getLimit());
        IPage iPage = baseMapper.page(page, processDefinitionVO);
        List records = iPage.getRecords();
        List<ProcessDefinitionVO> list = new ArrayList<>();
        for (Object record : records) {
            ProcessDefinitionEntityImpl processDefinitionEntity = (ProcessDefinitionEntityImpl) record;
            processDefinitionVO = new ProcessDefinitionVO();
            ObjectConvert.copyVoToEntity(processDefinitionEntity,processDefinitionVO);
            processDefinitionVO.setSuspended(String.valueOf(processDefinitionEntity.isSuspended()));
            processDefinitionVO.setSuspendedName(!processDefinitionEntity.isSuspended()?"已激活":"已挂起");
            list.add(processDefinitionVO);
        }
        iPage.setRecords(list);
        return iPage;
    }

    /**
     * 部署流程
     */
    @Override
    public void deploy(ProcessDefinitionVO processDefinitionVO) {
        try {
            String filePath = activitiProperties.getFile()+ processDefinitionVO.getName()+Constant.BPMN20;


            DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
                    .name(processDefinitionVO.getName())
                    // 自定义关键字
                    .key(Constant.UPLOAD_DEPLOYMENT + processDefinitionVO.getKey())
                    .category("http://www.activiti.org/processdef");
            if (activitiProperties.getDeployment().isProjectManifestEnabled()) {
                // =================设置资源清单=================
                // 为了测试表中字段, 使用项目资源清单对应 PROJECT_RELEASE_VERSION_, 这个项目版本会保存在 ACT_RE_DEPLOYMENT 表的 PROJECT_RELEASE_VERSION_ 字段中, 同时更新 ACT_RE_PROCDEF 表的 APP_VERSION_ 字段
                // projectManifest 的使用可以在 org.activiti.engine.impl.bpmn.deployer.BpmnDeployer.setProcessDefinitionVersionsAndIds() 看到
                // - 如果设置资源清单, 则流程定义 (ProcessDefinitionEntity) 的版本号从 deployment 获取
                // - 如果未设置, 则流程定义将取 (ProcessDefinitionEntity) 最新版本 + 1, 即: latest.getVersion() + 1
                List<Deployment> list = repositoryService.createDeploymentQuery().deploymentName(processDefinitionVO.getName()).orderByDeploymenTime().desc().list();
                // **注意** 如果设置了项目资源清单版本, 则 isAutoDeploymentEnabled 的判断规则会失效, 判断逻辑改为对比数据库中项目资源清单版本号:
                // - 版本号相同则数据过滤不做处理
                // - 版本号不同则 ACT_RE_DEPLOYMENT 表新增一条记录, 版本号 +1, ACT_RE_PROCDEF 表会根据部署时的模型文件数量新增对应 n 条数据, 同时版本 +1
                // org.activiti.engine.impl.cmd.DeployCmd.deploymentsDiffer():
                // !deployment.getProjectReleaseVersion().equals(saved.getProjectReleaseVersion());

                // 这里我们如果存在相同部署名称, 可以把版本号提取出来, 赋值给项目资源清单属性, 从列表中取第一个获取版本号
                String version = CollectionUtils.isEmpty(list) ? "1" : String.valueOf((list.get(0).getVersion() + 1));
                deploymentBuilder.setProjectManifest(buildProjectManifest(processDefinitionVO.getName(), Constant.UPLOAD_DEPLOYMENT, version));
            }
            // 这里有个问题, 就是设置资源清单的话, deploymentBuilder 并没有设置 version 的方法, 第二次再部署的话, 会报错 act_re_procdef: UNIQUE KEY `ACT_UNIQ_PROCDEF` (`KEY_`,`VERSION_`,`TENANT_ID_`)
            // 原因就是上面说的设置资源清单版本号, 流程定义从 deployment 获取, 而 deployment 版本不会更新, 就会导致添加流程定义数据唯一索引重复: UNIQUE KEY
            // 开启 isAutoDeploymentEnabled 之后 deployment 会查询是否存在相同名称的部署信息:
            // 是否过滤重复, 默认为 false, 防止资源没有发生变化而再次执行部署方法产生的重复部署
            if (activitiProperties.getDeployment().isAutoDeploymentEnabled()) {
                // - false: 每次部署 ACT_RE_DEPLOYMENT 都会新增一条部署信息, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据
                // - true: 部署时会判断部署名称和流程定义文件与数据库中是否相同:
                // * -- 名称相同, 流程定义文件内容相同, 数据过滤不做处理
                // * -- 名称相同, 流程定义文件内容不同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号 +1 , ACT_RE_PROCDEF 表不会新增数据
                // * -- 名称不同, 流程定义文件内容相同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据, 同时版本 +1
                // * -- 名称不同, 流程定义文件内容不同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据, 同时版本也是 1
                deploymentBuilder.enableDuplicateFiltering();
                // 这里有点绕，注释有点啰嗦
            }
            deploymentBuilder.addBytes(filePath, processDefinitionVO.getXml().getBytes(StandardCharsets.UTF_8));
            // 通过流程文件需要满足这个后缀条件: "bpmn20.xml", "bpmn"
            // 图片需要满足: "png", "jpg", "gif", "svg"
            // 对应源码 org.activiti.engine.impl.bpmn.deployer.ResourceNameUtil
//            deploymentBuilder.deploymentProperty("engineVersion",1.0);
            Deployment deploy = deploymentBuilder.deploy();
        } catch (PersistenceException p) {
            // 由上面的 setProjectManifest 衍生问题: 通过模型部署后, 再来使用上传文件部署会报错 act_re_procdef: UNIQUE KEY `ACT_UNIQ_PROCDEF`, 反过来亦是如此
            // 原因是上传模型部署使用的是流程文件节点 process id 赋值给 KEY_ 字段, 然后上面设置了 projectManifest, 流程定义 (ProcessDefinitionEntity) 的版本号从 deployment 获取
            // 就出现 ACT_RE_PROCDEF 表 KEY_ 和 版本号相同的情况, 这里解决办法就是规定上传的文件名称和模型名称相同才行, 否则代表流程定义信息已存在
            if (p.getCause() instanceof SQLIntegrityConstraintViolationException && p.getCause().getMessage().contains("Duplicate entry")) {
                throw new ActivitiBusinessException("流程定义信息已存在, 若要更新版本, 请将上传的部署文件名称和模型名称设为相同");
            }
        } catch (Exception e) {
            log.error("部署失败", e);
            throw new ActivitiBusinessException("部署失败");
        }
    }

    @Override
    public void deployFile(MultipartFile file, ProcessDefinitionVO processDefinitionVO) {
        if (file.isEmpty()) {
            throw new NullPointerException("部署压缩包不能为空");
        }
        try {
            DeploymentBuilder deploymentBuilder = repositoryService.createDeployment()
                    .name(processDefinitionVO.getName())
                    // 自定义关键字
                    .key(Constant.UPLOAD_DEPLOYMENT + processDefinitionVO.getKey())
                    .category("http://www.activiti.org/processdef");
            if (activitiProperties.getDeployment().isProjectManifestEnabled()) {
                // =================设置资源清单=================
                // 为了测试表中字段, 使用项目资源清单对应 PROJECT_RELEASE_VERSION_, 这个项目版本会保存在 ACT_RE_DEPLOYMENT 表的 PROJECT_RELEASE_VERSION_ 字段中, 同时更新 ACT_RE_PROCDEF 表的 APP_VERSION_ 字段
                // projectManifest 的使用可以在 org.activiti.engine.impl.bpmn.deployer.BpmnDeployer.setProcessDefinitionVersionsAndIds() 看到
                // - 如果设置资源清单, 则流程定义 (ProcessDefinitionEntity) 的版本号从 deployment 获取
                // - 如果未设置, 则流程定义将取 (ProcessDefinitionEntity) 最新版本 + 1, 即: latest.getVersion() + 1
                List<Deployment> list = repositoryService.createDeploymentQuery().deploymentName(processDefinitionVO.getName()).orderByDeploymenTime().desc().list();
                // **注意** 如果设置了项目资源清单版本, 则 isAutoDeploymentEnabled 的判断规则会失效, 判断逻辑改为对比数据库中项目资源清单版本号:
                // - 版本号相同则数据过滤不做处理
                // - 版本号不同则 ACT_RE_DEPLOYMENT 表新增一条记录, 版本号 +1, ACT_RE_PROCDEF 表会根据部署时的模型文件数量新增对应 n 条数据, 同时版本 +1
                // org.activiti.engine.impl.cmd.DeployCmd.deploymentsDiffer():
                // !deployment.getProjectReleaseVersion().equals(saved.getProjectReleaseVersion());

                // 这里我们如果存在相同部署名称, 可以把版本号提取出来, 赋值给项目资源清单属性, 从列表中取第一个获取版本号
                String version = CollectionUtils.isEmpty(list) ? "1" : String.valueOf((list.get(0).getVersion() + 1));
                deploymentBuilder.setProjectManifest(buildProjectManifest(processDefinitionVO.getName(), Constant.UPLOAD_DEPLOYMENT, version));
            }
            // 这里有个问题, 就是设置资源清单的话, deploymentBuilder 并没有设置 version 的方法, 第二次再部署的话, 会报错 act_re_procdef: UNIQUE KEY `ACT_UNIQ_PROCDEF` (`KEY_`,`VERSION_`,`TENANT_ID_`)
            // 原因就是上面说的设置资源清单版本号, 流程定义从 deployment 获取, 而 deployment 版本不会更新, 就会导致添加流程定义数据唯一索引重复: UNIQUE KEY
            // 开启 isAutoDeploymentEnabled 之后 deployment 会查询是否存在相同名称的部署信息:
            // 是否过滤重复, 默认为 false, 防止资源没有发生变化而再次执行部署方法产生的重复部署
            if (activitiProperties.getDeployment().isAutoDeploymentEnabled()) {
                // - false: 每次部署 ACT_RE_DEPLOYMENT 都会新增一条部署信息, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据
                // - true: 部署时会判断部署名称和流程定义文件与数据库中是否相同:
                // * -- 名称相同, 流程定义文件内容相同, 数据过滤不做处理
                // * -- 名称相同, 流程定义文件内容不同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号 +1 , ACT_RE_PROCDEF 表不会新增数据
                // * -- 名称不同, 流程定义文件内容相同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据, 同时版本 +1
                // * -- 名称不同, 流程定义文件内容不同, ACT_RE_DEPLOYMENT 表新增一条记录, 版本号是 1, ACT_RE_PROCDEF 会根据部署时的模型文件数量新增对应 n 条数据, 同时版本也是 1
                deploymentBuilder.enableDuplicateFiltering();
                // 这里有点绕，注释有点啰嗦
            }
            //压缩流
            ZipInputStream zip = new ZipInputStream(file.getInputStream());
            deploymentBuilder.addZipInputStream(zip);
            //设置部署流程名称
            deploymentBuilder.name(processDefinitionVO.getName());
            //部署流程
            Deployment deploy = deploymentBuilder.deploy();
        } catch (PersistenceException p) {
            // 由上面的 setProjectManifest 衍生问题: 通过模型部署后, 再来使用上传文件部署会报错 act_re_procdef: UNIQUE KEY `ACT_UNIQ_PROCDEF`, 反过来亦是如此
            // 原因是上传模型部署使用的是流程文件节点 process id 赋值给 KEY_ 字段, 然后上面设置了 projectManifest, 流程定义 (ProcessDefinitionEntity) 的版本号从 deployment 获取
            // 就出现 ACT_RE_PROCDEF 表 KEY_ 和 版本号相同的情况, 这里解决办法就是规定上传的文件名称和模型名称相同才行, 否则代表流程定义信息已存在
            if (p.getCause() instanceof SQLIntegrityConstraintViolationException && p.getCause().getMessage().contains("Duplicate entry")) {
                throw new ActivitiBusinessException("流程定义信息已存在, 若要更新版本, 请将上传的部署文件名称和模型名称设为相同");
            }
        } catch (Exception e) {
            log.error("部署失败", e);
            throw new ActivitiBusinessException("部署失败");
        }
    }

    @Override
    public String deleteById(String id,String deploymentId) {
        List<Deployment> list = repositoryService.createDeploymentQuery().deploymentId(deploymentId).list();
        if (list.size() <= 0 ) {
            return "流程定义未找到";
        }
        //根据部署id删除流程部署
        repositoryService.deleteDeployment(deploymentId);

        return "删除成功";
    }

    @Override
    public String show(String deploymentId, String resourceName) {
        InputStream is = repositoryService.getResourceAsStream(deploymentId, resourceName);
        byte[] xmlBytes;
        try {
            xmlBytes = IOUtils.toByteArray(is);
        } catch (IOException e) {
            throw new ActivitiBusinessException("解析部署流程文件失败");
        }
        try {
            return XmlUtil.formatXml(new String(xmlBytes, StandardCharsets.UTF_8));
        } catch (Exception e) {
            log.info("格式化 xml 失败");
        }
        // 格式化失败的话返回原始数据
        return new String(xmlBytes, StandardCharsets.UTF_8);
    }

    @Override
    public boolean verify(ProcessDefinitionVO processDefinitionVO) {
        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
        // 根据模型名称模糊查询
        if (StringUtils.isNotBlank(processDefinitionVO.getName())) {
            query.processDefinitionNameLike(processDefinitionVO.getName());
        }
        if (StringUtils.isNotBlank(processDefinitionVO.getKey())) {
            query.processDefinitionKeyLike(processDefinitionVO.getKey());
        }
        return query.list().size() > 0;
    }



    @Override
    public boolean exchangeToModel(String id, Integer designType) {
        // 根据 id 获取流程定义信息
        ProcessDefinition definition = repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult();
        if (null == definition) {
            throw new ActivitiBusinessException("流程定义信息不存在");
        }
        SysUser user = (SysUser) StpUtil.getSession().get("user");
        // 这里看需求, 转换的模型存在是否更新版本替换, 我这里是调用新增模型了, 不允许重复, 如果要更新替换要补充一下逻辑
        ActivitiModel dto = new ActivitiModel();
        dto.setKey(definition.getKey());
        dto.setName(definition.getName());
        dto.setAuthor(user.getUsername());
        dto.setCategory(definition.getCategory());
        dto.setDescription(definition.getDescription());
        dto.setDesignType(designType);
        dto.setGenerateProcess(false);
        // 保存模型信息
        modelService.save(dto);

        // 根据 id 获取流程定义的 bpmn 文件
        BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
        // 添加流程设计文件, 注意, activiti-modeler 保存的是 json, bpmn-js 保存的是 xml
        byte[] modelData;
        if (designType == 1) {
            // 使用 BpmnJsonConverter 把 bpmnModel 转换为 jsonNode
            ObjectNode objectNode = new BpmnJsonConverter().convertToJson(bpmnModel);
            modelData = objectNode.toString().getBytes(StandardCharsets.UTF_8);
        } else {
            // 使用 BpmnXMLConverter 把 bpmnModel 转换为 xml
            modelData = new BpmnXMLConverter().convertToXML(bpmnModel);
        }
        repositoryService.addModelEditorSource(dto.getId(), modelData);
        return true;
    }

    @Override
    public boolean suspend(SuspendDefinition dto) {
        return activate(false, dto);
    }

    @Override
    public boolean activate(SuspendDefinition dto) {
        return activate(true, dto);
    }

    public boolean activate(boolean isActivate, SuspendDefinition dto) {
        Date date = null;
        if (StringUtils.isNotEmpty(dto.getActivationDate())) {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
            LocalDate localDate = LocalDate.parse(dto.getActivationDate(), formatter);
            date = Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
        }
        try {
            if (isActivate) {
                // 激活, 并且激活关联的实例
                repositoryService.activateProcessDefinitionById(dto.getId(), dto.getActivateProcessInstances(), date);
            } else {
                // 挂起
                repositoryService.suspendProcessDefinitionById(dto.getId(), dto.getActivateProcessInstances(), date);
            }
        } catch (Exception e) {
            log.error(isActivate ? "激活流程失败" : "挂起流程失败", e);
            throw new ActivitiBusinessException(isActivate ? "激活流程失败" : "挂起流程失败");
        }
        return true;
    }


    public IPage processInstance(ProcessInstanceVO flowInfo){
        ProcessInstanceQuery condition = runtimeService.createProcessInstanceQuery();
        if (StringUtils.isNotEmpty(flowInfo.getBusinessKey())) {
            condition.processInstanceBusinessKey(flowInfo.getBusinessKey());
        }
        if (StringUtils.isNotEmpty(flowInfo.getName())) {
            condition.processDefinitionName(flowInfo.getName());
        }

        List<ProcessInstance> processList = condition.orderByProcessDefinitionId().desc().listPage(flowInfo.getPage(), flowInfo.getLimit());
        List<ProcessInstanceVO> flows = new ArrayList<>();
       /* processList.stream().forEach(p -> {
            ProcessInstanceVO info = new ProcessInstanceVO();
            info.setProcessInstanceId(p.getProcessInstanceId());
            info.setBusinessKey(p.getBusinessKey());
            info.setName(p.getProcessDefinitionName());
            info.setStartTime(p.getStartTime());
            info.setStartUserId(p.getStartUserId());
            info.setSuspended(String.valueOf(p.isSuspended()));
            info.setSuspendedName(!p.isSuspended()?"已激活":"已挂起");
            info.setEnded(p.isEnded());
            // 查看当前活动任务
            List<Task> tasks =  taskService.createTaskQuery().processInstanceId(p.getProcessInstanceId()).list();
            if (tasks.size() > 0) {
                String taskName = "";
                String assignee = "";
                for (Task t : tasks) {
                    taskName += t.getName() + ",";
                    assignee += t.getAssignee() + ",";
                }
                taskName = taskName.substring(0, taskName.length() -1);
                assignee = assignee.substring(0, assignee.length() -1);
                info.setCurrentTask(taskName);
                info.setAssignee(assignee);
            }
            flows.add(info);
        });*/
        int total = condition.orderByProcessDefinitionId().desc().list().size();
        IPage<ProcessInstanceVO> page = new Page<>(flowInfo.getPage(), flowInfo.getLimit());
        page.setRecords(flows);
        page.setTotal(total);

        return page;
    }

    @Override
    public void startProcess(String id) {
        List<ProcessDefinition> list = repositoryService.createProcessDefinitionQuery().processDefinitionId(id).list();

        if (list.size() != 1) {
            throw new ActivitiBusinessException("流程定义不存在");
        }
        SysUser user = (SysUser) StpUtil.getSession().get("user");
        Map<String, Object> v = new HashMap<>();
        v.put("name", user.getUsername());
        //通过流程定义ID启动一个流程实例
        ProcessInstance processInstance = runtimeService.startProcessInstanceById(id,v);
        log.info("流程实例：{}", processInstance);
    }


    private ProjectManifest buildProjectManifest(String name, String description, String version) {
        SysUser user = (SysUser) StpUtil.getSession().get("user");
        ProjectManifest manifest = new ProjectManifest();
        manifest.setId(IdUtil.getId());
        manifest.setCreatedBy(user.getUsername());
        manifest.setCreationDate(DateUtil.now());
        manifest.setLastModifiedBy(user.getUsername());
        manifest.setLastModifiedDate(DateUtil.now());
        manifest.setName(name);
        manifest.setDescription(description);
        manifest.setVersion(version);
        return manifest;
    }
}
